﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using Chefbook.HelperMethods;
using System.ComponentModel;


namespace Chefbook.DataObjects
{
    /// <summary>
    /// Represents a category in a hierarchy structure that can contain items
    /// </summary>
    public class Category : INotifyPropertyChanged
    {
        private ObservableCollection<Category> subcategories = new ObservableCollection<Category>();
        private ObservableCollection<Recipe> recipes = new ObservableCollection<Recipe>();
        private ObservableCollection<Recipe> allRecipes = new ObservableCollection<Recipe>();


        private Category parent;
        private string name;

        /// <summary>
        /// Constructor for a new category
        /// </summary>
        public Category()
        {
            name = string.Empty;
        }
        /// <summary>
        /// Constructor for a category with a given parent and category name
        /// </summary>
        /// <param name="parent">Category that this Category is contained in.  Null if root category</param>
        public Category(Category parent)
        {
            Parent = parent;
            parent.PerformIfNotNull(() => parent.subcategories.AddIfNewAndNotNull(this));
            Name = string.Empty;
        }

        /// <summary>
        /// Gets or sets the name of the category
        /// </summary>
        public string Name
        {
            get { return name; }
            set
            {
                if (name != value)
                {
                    name = value.ThrowIfNull("Name");
                    OnPropertyChanged("Name");
                }
            }
        }

        /// <summary>
        /// Gets whether this category is the root category
        /// </summary>
        public bool IsRoot { get { return parent == null; } }

        /// <summary>
        /// Removes this folder from its parent
        /// </summary>
        public void Delete()
        {
            this.Parent.PerformIfNotNull(() => this.parent.subcategories.Remove(this));
            this.Parent = null;
            List<Recipe> recipesToRemoveFrom = new List<Recipe>(recipes);
            foreach (var recipe in recipesToRemoveFrom)
            {
                recipe.RemoveCategory(this);
            }
        }

        /// <summary>
        /// Gets or sets the parent category
        /// </summary>
        public Category Parent
        {
            get { return parent; }
            set
            {
                if (parent != value)
                {
                    parent.PerformIfNotNull(() => this.parent.subcategories.Remove(this));
                    this.parent = value;
                    value.PerformIfNotNull(() => value.subcategories.AddIfNewAndNotNull(this));
                    OnPropertyChanged("Parent");
                }
            }
        }

        /// <summary>
        /// Adds a recipe to this category's direct recipes
        /// </summary>
        /// <param name="recipeToAdd">The recipe to add</param>
        public void AddRecipe(Recipe recipeToAdd)
        {
            recipeToAdd.ThrowIfNull("recipeToAdd");
            if (!recipes.Contains(recipeToAdd))
            {
                recipes.Add(recipeToAdd);
                allRecipes.AddIfNewAndNotNull(recipeToAdd);
                var currentCategory = this;
                while (currentCategory.parent != null)
                {
                    currentCategory.parent.allRecipes.AddIfNewAndNotNull(recipeToAdd);
                    currentCategory = currentCategory.parent;
                }
                recipeToAdd.AddCategory(this);
            }
            
        }

        /// <summary>
        /// Removes a recipe from this category
        /// </summary>
        /// <exception cref="ArgumentException">Thrown if category doesn't contain recipeToRemove</exception>
        /// <exception cref="ArgumentNullException">Thrown if recipeToRemove is null</exception>
        /// <param name="recipeToRemove">The recipe to remove</param>
        public void RemoveRecipe(Recipe recipeToRemove)
        {
            recipeToRemove.ThrowIfNull("recipeToRemove");
            ParameterValidation.ValidateArgument(
                ()=> recipes.Contains(recipeToRemove), "recipeToRemove");

            recipes.Remove(recipeToRemove);
            allRecipes.Remove(recipeToRemove);
            removeFromParentsAllRecipes(recipeToRemove);
            if (recipeToRemove.Categories.Contains(this))
            { recipeToRemove.RemoveCategory(this); }
        }
        
        /// <summary>
        /// Adds a category as a sub of this one
        /// </summary>
        /// <param name="subcategory">The Category to add as a sub</param>
        public void AddSubcategory(Category subcategory)
        {
            subcategory.ThrowIfNull("sub");
            subcategory.parent = this;
            subcategories.AddIfNewAndNotNull(subcategory);
        }

        /// <summary>
        /// Removes a subcategory from this category
        /// </summary>
        /// <param name="subcategory">The subcategory to remove</param>
        public void RemoveSubcategory(Category subcategory)
        {
            subcategory.ThrowIfNull("sub");
            ParameterValidation.ValidateArgument(() => subcategories.Contains(subcategory), "sub");
            subcategories.Remove(subcategory);
            subcategory.parent = null;
        }

        /// <summary>
        /// Gets a readonlylist of direct subcategories of this category
        /// </summary>
        public ReadOnlyObservableCollection<Category> Subcategories
        {
            get { return new ReadOnlyObservableCollection<Category>(subcategories); }
        }

        /// <summary>
        /// Gets a readonly list of recipes contained directly in this category 
        /// not one of its subcategories
        /// </summary>
        public ReadOnlyObservableCollection<Recipe> DirectRecipes
        {
            get { return new ReadOnlyObservableCollection<Recipe>(recipes); }
        }

        /// <summary>
        /// Gets readonly list of all recipes contained in this category or one of its subcategories
        /// </summary>
        public ReadOnlyObservableCollection<Recipe> AllRecipes
        {
            get { return new ReadOnlyObservableCollection<Recipe>(allRecipes); }
        }

        /// <summary>
        /// Returns the name of this Category
        /// </summary>
        /// <returns>The name of the Category</returns>
        public override string ToString()
        {
            return Name;
        }

        /// <summary>
        /// Checks whether this Category has value equality to the parameter passed in
        /// </summary>
        /// <param name="value">The Category to check this against</param>
        /// <returns>True of they are equal in value, otherwise false</returns>
        public bool ValueEquals(object value)
        {
            var cat = value as Category;
            if (cat == null) return false;
            if (!(this.Name.NullSafeEquals(cat.Name)))
            { return false; }

            if (this.recipes.Count != cat.recipes.Count)
            { return false; }
            foreach (var pair in this.recipes.Zip(cat.recipes, (i1, i2) => new { value1 = i1, value2 = i2 }))
            {
                if (!pair.value1.ValueEquals(pair.value2))
                { return false; }
            }

            if (this.subcategories.Count != cat.subcategories.Count)
            { return false; }
            foreach (var pair in this.subcategories.Zip(cat.subcategories, (i1, i2) => new { value1 = i1, value2 = i2 }))
            {
                if (!pair.value1.ValueEquals(pair.value2))
                { return false; }
            }

            return true;
        }

        /// <summary>
        /// Raised when a property on this object has a new value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        /// <summary>
        /// Raises this object's PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">The property that has a new value.</param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private void removeFromParentsAllRecipes(Recipe recipeToRemove)
        {
            var currentCategory = this;
            while (currentCategory.parent != null)
            { //Walk through path of all parents and remove from their AllRecipes if needed
                currentCategory = currentCategory.parent;
                //Check to see if recipe exists in some other sub category
                bool existElsewhere = false;
                if (currentCategory.DirectRecipes.Contains(recipeToRemove))
                { existElsewhere = true; }
                foreach (var sub in currentCategory.Subcategories)
                {
                    if (sub.AllRecipes.Contains(recipeToRemove))
                    { existElsewhere = true; }
                }
                if (!existElsewhere) //Doesn't exist elsewhere so remove from this ancestor's AllRecipes
                { currentCategory.allRecipes.Remove(recipeToRemove); }
            }
        }
    }
}